/*!
  \file
  Header file for a heterogeneous hypervolume
*/

/*
  Copyright (c) 2011, President & Fellows of Harvard College
  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  * Neither the name of the Harvard College nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


#ifndef SCIGPU_HHV_HHVOLUME_BASE_HPP
#define SCIGPU_HHV_HHVOLUME_BASE_HPP

#include <memory>

#include "H5Cpp.h"


#include "dimensionobject.hpp"
#include "bufferobject.hpp"
#include "simplebuffer.hpp"

#include "hhv_h5.hpp"


/*!
  \mainpage


  The heterogeneous hypervolume is a class developed to demonstrate how
  heterogeneous programs can be written to exploit heterogeneous
  hardware.
  This class was inspired by the authors' experience in porting software
  to run in NVIDIA's CUDA environment.
  In every case, data management was a far bigger issue than writing the
  kernels which performed the actual computation.

  The heterogeneous hypervolume is based on the idea of using a class
  hierarchy to separate data from metadata, and different execution
  environments from each other.
  For example, suppose we are performing matrix computations.
  A matrix's metadata are the number of rows and columns, and the
  type of data stored.
  The actual data may be stored on a variety of devices - the CPU,
  GPUs, Cell etc.
  We realised that it made sense to have an abstract base class,
  which would describe the <em>concept</em> of a matrix (and hold
  the metadata), and then have specialised child classes to hold the
  data on particular devices.
  Moving data between devices becomes the responsibility of the
  language's type system.

  A similar approach can be taken with the algorithms used to
  manipulate the data.
  The concept of matrix multiplication is described in an abstract
  base class, while child classes call the appropriate \c GEMM library.

  \section Licencing

  This code is released under the terms of the BSD licence.
  See the \c LICENCE.txt file in the top directory for the licence text.

  
*/

namespace SciGPU {
  namespace HHV {
    
    //! Abstract base class describing a hypervolume
    /*!
      This is the main class describing a heterogeneous
      hypervolume.
      It is an abstract base class, which must be implemented
      for specific execution devices.
      
      To enable hypervolumes from different execution
      environments to communicate, each HHVolume object
      maintains an internal buffer.
      This holds data in a normalised form - a contiguous
      1D array of hypervolume elements (hyxels) where the
      first index of the hypervolume runs fastest.
      Implementations of \c operator() are provided for up to
      four dimensions which allow access to this buffer.
      The type of the buffer object must be specified at
      construction, and defaults to SciGPU::HHV::SimpleBuffer.

      \attention
      The data on the execution device are generally considered
      to be correct, and the buffer is only guaranteed to be
      synchronised immediately following a call to HHVolume::Recv
      Similarly, changes to the buffer must be explicitly
      sent to the execution device using HHVolume::Send.
      

      

      @tparam T     The type of the data stored
      @tparam nDims The number of dimensions in the volume
    */
    template<typename T, unsigned int nDims>
    class HHVolume {
    public:
      
      //! Exposes the type of \a T
      typedef T value_type;

      //! Make all HHVolumes friends
      template<typename U, const unsigned int n> friend class HHVolume;

      //! Constructor with buffer
      HHVolume( const BufferObject<T>& buff = SimpleBuffer<T>() ) : dims(),
                                                                    buffer(buff.clone()) {};
      
      
      //! Destructor
      virtual ~HHVolume( void ) {
        delete (this->buffer);
      };
      
      // ------------------------------------
      
      //! Allocator
      void Allocate( const DimensionObject<nDims>& myDims ) {
        /*!
          Allocates both buffer and device memory to store
          the hypervolume defined by \a myDims.
        */
        
        // Check for identical size
        if( myDims == this->dims ) {
          return;
        }
        
        // Resize the buffer
        this->buffer->Allocate( myDims.hyxels() );
        
        // Resize the device data
        this->AllocateDevice( myDims );
        
        // Set the new number of dimensions
        this->dims = myDims;
      }
      
      // ----------------------------------
      
      //! Returns number of hypervolume elements allocated
      size_t hyxels( void ) const {
        return( this->dims.hyxels() );
      }
      
      //! Returns the hypervolume dimensions
      const DimensionObject<nDims> dimensions( void ) const {
        return( this->dims );
      }
      
      // ----------------------------------
      
      //! Send data from the internal buffer to the computational device
      /*!
        This method should copy data from the internal buffer to the
        computational device.
        Implementations of SciGPU::HHV::HHVolume must over-ride this.
      */
      virtual void Send( void ) = 0;
      
      //! Fetch data from the computational device to the internal buffer
      /*!
        This method should copy data from the computational device to the
        internal buffer.
        It is declared \const since the data on the device is regarded
        as correct, while the buffer has a weak consistency
        guarantee, and is declared \c mutable.
      */
      virtual void Recv( void ) const = 0;
      
      // ----------------------------------
      
      //! Access the start of the data (const)
      /*!
        This method should provide a \c const pointer to
        the start of the data on the execution device.
        Implementations of SciGPU::HHV::HHVolume must over-ride this.
      */
      virtual const T* start( void ) const = 0;
      
      //! Access the start of the data
      /*!
        This method should provide a pointer to
        the start of the data on the execution device.
        Implementations of SciGPU::HHV::HHVolume must over-ride this.
      */
      virtual T* start( void ) = 0;
      
      // ----------------------------------
      
      //! Copy method
      void Copy( const HHVolume& src ) {
        /*!
          This copies one heterogeneous hypervolume
          to another.
          The data types and number of dimensions must
          match, but the implementation types need not.
          The copy is made via the internal buffer,
          using the HHVolume::Send and HHVolume::Recv
          methods.
          Implementations for a particular execution
          environment should overload this, so that
          copies can be made without using the host
          buffer (and the associated HHVolume::Send
          and HHVolume::Recv calls).
         */
        if( &src != this ) {
          // Allocate our memory
          this->Allocate( src.dimensions() );
          
          // Get the src data into its buffer
          src.Recv();
          
          // Copy
          memcpy( this->buffer->start(),
                  src.buffer->start(),
                  src.hyxels() * sizeof(T) );
          
          // Send to our device
          this->Send();
        }
      }
      

      //! Conversion method
      template<typename U>
      void Convert( const HHVolume<U,nDims>& src ) {
        /*!
          This uses the buffer data to convert
          one type of data to another (and possibly on
          a different execution device).
          However, the number of dimensions must still
          match.
          Implementations for particular execution
          devices could over-ride this, so that use
          of the buffer (and associated HHVolume::Send
          and HHVolume::Recv calls) can be avoided.
        */
        if( &src != reinterpret_cast<void*>(this) ) {
          // Allocate our memory
          this->Allocate( src.dimensions() );

          // Get src data into its buffer
          src.Recv();

          // Do the buffer conversion
          const U* srcPtr = src.buffer->start();
          T* dstPtr = this->buffer->start();

          for( unsigned int i=0; i<src.hyxels(); i++ ) {
            *dstPtr++ = *srcPtr++;
          }

          // Send the new data
          this->Send();
        }
      }

      
      // ----------------------------------
      // The following act on the buffer
      
      //! Mutator for the buffer (1D)
      inline T& operator()( const unsigned int i0 ) {
        return( this->buffer->start()[ this->dims.Offset(i0) ] );
      }
      
      //! Mutator for the buffer (2D)
      inline 
      T& operator()( const unsigned int i0,
                     const unsigned int i1 ) {
        return( this->buffer->start()[ this->dims.Offset(i0,i1) ] );
      }
      
      //! Mutator for the buffer (3D)
      inline 
      T& operator()( const unsigned int i0,
                     const unsigned int i1,
                     const unsigned int i2 ) {
        return( this->buffer->start()[ this->dims.Offset(i0,i1,i2) ] );
      }
      
      //! Mutator for the buffer (4D)
      inline 
      T& operator()( const unsigned int i0,
                     const unsigned int i1,
                     const unsigned int i2,
                     const unsigned int i3 ) {
        return( this->buffer->start()[ this->dims.Offset(i0,i1,i2,i3) ] );
      }
      
      // ---------------------------------------
      
      //! Accessor for the buffer (1D)
      inline 
      T operator()( const unsigned int i0 ) const {
        return( this->buffer->start()[ this->dims.Offset(i0) ] );
      }
      
      //! Accessor for the buffer (2D)
      inline 
      T operator()( const unsigned int i0,
                    const unsigned int i1 ) const {
        return( this->buffer->start()[ this->dims.Offset(i0,i1) ] );
      }
      
      //! Accessor for the buffer (3D)
      inline 
      T operator()( const unsigned int i0,
                    const unsigned int i1,
                    const unsigned int i2 ) const {
        return( this->buffer->start()[ this->dims.Offset(i0,i1,i2) ] );
      }
      
      //! Accessor for the buffer (4D)
      inline 
      T operator()( const unsigned int i0,
                    const unsigned int i1,
                    const unsigned int i2,
                    const unsigned int i3 ) const {
        return( this->buffer->start()[ this->dims.Offset(i0,i1,i2,i3) ] );
      }
      
      
      
      // ------------------------------
      
      //! Output to HDF5 file
      void WriteHDF5( const std::string& filename,
                      const std::string& varName = "default" ) const {
        /*!
          Writes the hypervolume to a variable \a varName
          into the root group of a newly created HDF5 file
          \a filename
        */
        // Create the file
        H5::H5File file( filename, H5F_ACC_TRUNC );
        
        // Access the root group
        H5::Group rootGroup = file.openGroup( "/" );
        
        // Write it out
        this->WriteHDF5group( rootGroup, varName );
      }
      
      //! Output to HDF5 group
      void WriteHDF5group( H5::Group& parentGroup,
                           const std::string& varName ) const {
        /*!
          Writes the contents of the hypervolume to an HDF5
          group, with a specified variable name.
          @param     parentGroup The group where the hypervolume is to be written
          @param[in] varName     The name of the variable to create within \a parentGroup
        */
        // Retrieve the data
        this->Recv();
        
        // Create the group
        H5::Group myGroup = parentGroup.createGroup( varName );
        
        
        // Define the array size, and create dataset
        hsize_t dimsf[nDims];
        for( unsigned int i=0; i<nDims; i++ ) {
          dimsf[i] = this->dims[nDims-i-1];
        }
        
        H5::DataSpace dataspace( nDims, dimsf );
        
        H5::DataSet dataset = myGroup.createDataSet( "real",
                                                     HHVH5<T>::GetH5Type(),
                                                     dataspace );
        
        dataset.write( this->buffer->start(), HHVH5<T>::GetH5Type() );
      }
      
      // ------------------------------
      
      //! Input from HDF5
      void ReadHDF5( const std::string& filename,
                     const std::string& varName ) {
        /*!
          Reads the variable \a varName from the root group
          of HDF5 file \a filename into the hypervolume.
        */
        // Open the file
        H5::H5File file( filename, H5F_ACC_RDONLY );
        
        // Get the root group
        H5::Group rootGroup = file.openGroup( "/" );
        
        std::string myVarName;
        
        if( !varName.empty() ) {
          myVarName = varName;
        } else {
          // Work out how many objects are present
          size_t numObjs = rootGroup.getNumObjs();
          std::cout << __FUNCTION__
                    << ": Found " << numObjs << " objects" << std::endl;
          
          // Get the name of the first group
          for( size_t i=0; i<numObjs; i++ ) {
            H5G_obj_t objType = rootGroup.getObjTypeByIdx( i );
            if( objType == H5G_GROUP ) {
              myVarName = rootGroup.getObjnameByIdx( i );
              std::cout << __FUNCTION__
                        << ": Found group " << myVarName << std::endl;
            }
          }
          
          if( myVarName.empty() ) {
            throw std::runtime_error( "No groups found" );
          }
        }
        
        this->ReadHDF5group( rootGroup, myVarName );
      }
      
      
      //! Input from HDF5 (will read first group)
      void ReadHDF5( const std::string& filename ) {
        this->ReadHDF5( filename, "" );
      }
      
      
      //! Input from HDF5 group
      void ReadHDF5group( const H5::Group& parentGroup,
                          const std::string& varName ) {
        /*!
          Reads the variable \a varName from the given
          HDF5 group \a parentGroup into the hypervolume
        */
        // Open the group from the parent
        H5::Group myGroup = parentGroup.openGroup( varName );
        
        // Open the dataset
        H5::DataSet dataset = myGroup.openDataSet( "real" );
        
        // Check the type
        if( ! (dataset.getDataType() == HHVH5<T>::GetH5Type()) ) {
          throw std::runtime_error( "Incompatible datatype" );
        }
        
        // Get the dataspace
        H5::DataSpace dataspace = dataset.getSpace();
        
        // Check number of dimensions
        if( nDims != dataspace.getSimpleExtentNdims() ) {
          throw std::runtime_error( "Incompatible number of dimensions" );
        }
        
        // Get the dimensions
        hsize_t dimsf[nDims];
        dataspace.getSimpleExtentDims( dimsf, NULL );
        DimensionObject<nDims> dims;
        for( unsigned int i=0; i<nDims; i++ ) {
          dims[i] = dimsf[nDims-i-1];
        }
        
        // Allocate space
        this->Allocate( dims );
        
        // Read the data
        dataset.read( this->buffer->start(), HHVH5<T>::GetH5Type() );
        
        // Send it to the processing device
        this->Send();
      }
      
      
      
      
      
      // ==================================================
    protected:
      //! The dimensions of the volume
      DimensionObject<nDims> dims;
      
      //! Buffer object
      mutable BufferObject<T>* buffer;
      
      //! Allocate memory on computational device
      /*!
        This method should allocate memory on the execution device
        to hold the hypervolume specified by \a myDims.
        Implementations of SciGPU::HHV::HHVolume must over-ride this.
        \pre HHVolume::dims will still store the 'old' dimensions during this call
       */
      virtual void AllocateDevice( const DimensionObject<nDims>& myDims ) = 0;
      
      
      //! Copy constructor
      HHVolume( const HHVolume& src ) : dims(src.dims),
                                        buffer(src.buffer->clone()) {
        /*!
          This is protected since this is an abstract base class.
          Furthermore, the buffer is not copied, merely allocated
        */
        this->buffer->Allocate( src.dims.hyxels() );
      }
      
      //! Assignment operator
      HHVolume& operator=( const HHVolume& src ) {
        /*!
          Protected, since this is the abstract base class.
          Also, the buffer is merely allocated
        */
        
        if( src != *this ) {
          // Change to the new type of buffer
          BufferObject<T>* tmp = src->buffer.clone();
          delete this->buffer;
          this->buffer = tmp;
          
          this->buffer->Allocate( src.dims.hyxels() );
          
        }
        
        return( *this );
      }
      
      // ==================================================
    private:  
      
    };
    
    

  }
}



#endif
